package pinserver;

import java.io.*;
import java.net.*;
import java.math.BigInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Packet
{
    //Variabele die de grootte van de packet bijhoudt

    private int msgSize;
    //Variabele die de huidige leespositie van de packet bijhoudt
    private int readPos;
    //Byte array waarin alle data (incl. header) komt te staan
    private byte[] buffer;

    //Maximale grootte van een packet
    final int PACKET_MAXSIZE = 128;

    public Packet()
    {
        try
        {
            //De byte array initialiseren met de maximale grootte
            buffer = new byte[PACKET_MAXSIZE];
            reset();
        } catch (Exception e)
        {
            Main.drukaf("EQ-Exception while creating packet: " + e);
        }
    }

    /*
     * De grootte en leespositie initialiseren/resetten
     */
    public void reset()
    {
        msgSize = 0;
        readPos = 2;
    }

    /*
     * De leespositie resetten
     */
    public void rewind()
    {
        readPos = 2;
    }

    /*
     * Controleren of de hoeveelheid bytes aan de packet kunnen worden toegevoegd.
     */
    public boolean canAdd(int size)
    {
        if (size + readPos < PACKET_MAXSIZE - 16)
        {
            return true;
        } else
        {
            return false;
        }
    }

    /*
     * De packet versturen naar de gewenste socket.
     * Ook wordt hier de packet grootte toegevoegd aan het begin
     * van de packet.
     */
    public void sendToServer(Socket socket) throws IOException
    {
        //De grootte van de bruikbare data in de eerste twee bytes opslaan
        buffer[0] = (byte) (msgSize);
        buffer[1] = (byte) (msgSize >> 8);

        //Alle data met RSA versleutelen
        RSAencrypt();

        OutputStream output = socket.getOutputStream();

        //Alle data versturen
        output.write(buffer);
        output.flush();
    }

    /*
     * (Data van) een packet ontvangen van een socket.
     * Er wordt gebruik gemaakt van een "blocking" systeem. Dit houdt in dat
     * de applicatie bij deze functie blijft hangen totdat er een packet is
     * ontvangen. Het is daarom van belang dat deze functie in een aparte thread
     * wordt aangeroepen.
     */
    public boolean receiveFromServer(Socket socket) throws IOException
    {
        reset();
        InputStream input = socket.getInputStream();
        //Alle data inlezen (altijd een grootte van 128 bytes)
        input.read(buffer);

        //Alle data ontsleutelen
        RSAdecrypt();

        //De grootte van de bruikbare data uitlezen
        int datasize = buffer[0] | buffer[1] << 8;
        if (datasize <= 0)
        {
            return false;
        } else
        {
            return true;
        }
    }

    /*
     * (Data van) een packet ontvangen van een BufferedInputStream.
     * Er wordt gebruik gemaakt van een "blocking" systeem. Dit houdt in dat
     * de applicatie bij deze functie blijft hangen totdat er een packet is
     * ontvangen. Het is daarom van belang dat deze functie in een aparte thread
     * wordt aangeroepen.
     */
    public boolean receiveFromBufferedInputStream(BufferedInputStream input) throws IOException
    {
        reset();
        //Alle data inlezen (altijd een grootte van 128 bytes)
        input.read(buffer);

        //Alle data ontsleutelen
        RSAdecrypt();

        //De grootte van de bruikbare data uitlezen
        int datasize = buffer[0] | buffer[1] << 8;
        if (datasize <= 0)
        {
            return false;
        } else
        {
            return true;
        }
    }

    /*
     * Een Header byte toevoegen aan de packet
     */
    public void addByte(byte value)
    {
        if (!canAdd(1))
        {
            return;
        }

        buffer[readPos++] = value;
        msgSize++;
    }

    /*
     * Een Header byte toevoegen aan de packet
     */
    public void addByte(int value)
    {
        if (!canAdd(1))
        {
            return;
        }

        buffer[readPos++] = (byte) value;
        msgSize++;
    }

    /*
     * Een Header toevoegen aan de packet
     */
    public void addHeader(Header header)
    {
        addByte(header.value());
    }

    /*
     * Een Error toevoegen aan de packet
     */
    public void addError(Error error)
    {
        addByte(error.value());
    }

    /*
     * Een integer toevoegen aan de packet
     */
    public void addInt(int value)
    {
        if (!canAdd(4))
        {
            return;
        }

        buffer[readPos++] = (byte) (value >>> 24);
        buffer[readPos++] = (byte) (value >>> 16);
        buffer[readPos++] = (byte) (value >>> 8);
        buffer[readPos++] = (byte) (value);
        msgSize += 4;
    }

    /*
     * Een double toevoegen aan de packet
     */
    public void addDouble(double value)
    {
        long rawBits = Double.doubleToRawLongBits(value);
        if (!canAdd(8))
        {
            return;
        }
        buffer[readPos++] = (byte) ((rawBits >>> 56) & 0xff);
        buffer[readPos++] = (byte) ((rawBits >>> 48) & 0xff);
        buffer[readPos++] = (byte) ((rawBits >>> 40) & 0xff);
        buffer[readPos++] = (byte) ((rawBits >>> 32) & 0xff);
        buffer[readPos++] = (byte) ((rawBits >>> 24) & 0xff);
        buffer[readPos++] = (byte) ((rawBits >>> 16) & 0xff);
        buffer[readPos++] = (byte) ((rawBits >>> 8) & 0xff);
        buffer[readPos++] = (byte) ((rawBits) & 0xff);
        msgSize += 8;

    }

    /*
     * Een long toevoegen aan de packet
     */
    public void addLong(long value)
    {
        if (!canAdd(4))
        {
            return;
        }

        buffer[readPos++] = (byte) (value);
        buffer[readPos++] = (byte) (value >> 8);
        buffer[readPos++] = (byte) (value >> 16);
        buffer[readPos++] = (byte) (value >> 24);
        msgSize += 4;
    }

    /*
     * Een string toevoegen aan de packet.
     * Hierbij wordt eerst de grootte van de string als integer toegevoegd
     * aan de packet. Vervolgens wordt elk character in de string als byte
     * toegevoegd.
     */
    public void addString(String string)
    {
        addInt(string.length());
        for (int i = 0; i < string.length(); i++)
        {
            buffer[readPos++] = (byte) string.charAt(i);
        }
        msgSize += string.length();
    }

    /*
     * Een byte uitlezen
     */
    public byte readByte()
    {
        return buffer[readPos++];
    }

    /*
     * Een Header uitlezen
     */
    public Header readHeader()
    {
        return Header.get(readByte());
    }

    /*
     * Een Error uitlezen
     */
    public Error readError()
    {
        return Error.get(readByte());
    }

    /*
     * Een integer uitlezen
     */
    public int readInt()
    {
        int v = (buffer[readPos] << 24) | ((buffer[readPos + 1] & 0xff) << 16)
               | ((buffer[readPos + 2] & 0xff) << 8) | (buffer[readPos + 3] & 0xff);
        readPos += 4;
        return v;
    }

    /*
     * Een integer uitlezen
     */
    public double readDouble()
    {
        long v = (long) ((long) (buffer[readPos] & 0xFF) << 56 |
                (long) (buffer[readPos + 1] & 0xFF) << 48 |
                (long) (buffer[readPos + 2] & 0xFF) << 40 |
                (long) (buffer[readPos + 3] & 0xFF) << 32 |
                (long) (buffer[readPos + 4] & 0xFF) << 24 |
                (long) (buffer[readPos + 5] & 0xFF) << 16 |
                (long) (buffer[readPos + 6] & 0xFF) << 8 |
                (long) (buffer[readPos + 7] & 0xFF) << 0);
        readPos += 8;
        return Double.longBitsToDouble(v);
    }

    /*
     * Een long uitlezen
     */
    public long readLong()
    {
        long v = ((buffer[readPos] & 0xFF) |
                ((buffer[readPos + 1] & 0xFF) << 8) |
                ((buffer[readPos + 2] & 0xFF) << 16) |
                ((buffer[readPos + 3] & 0xFF) << 24));

        readPos += 4;
        return v;
    }

    /*
     * Een string uitlezen.
     * Eerst wordt de lengte van de string uitgelezen als integer. Vervolgens
     * wordt elke character als byte uitgelezen.
     */
    public String readString()
    {
        String string = "";
        int stringlength = readInt();
        for (int i = 0; i < stringlength; i++)
        {
            string = string + (char) buffer[readPos++];
        }
        return string;
    }

    /*
     * Alle data versleuten met de public key
     */
    public void RSAencrypt()
    {
        try
        {
            //public exponent
            BigInteger exponent = new BigInteger("65537");
            BigInteger modulus = new BigInteger("99379948379960362318133545416777134441212959077806925836047576484354847740695895722711224301417536998763774398137274853428838318449808212642812881950275506403998235108557228615154895317730035593809893475114123023601357966397062390155405161131857926830195384564871250195516504358455352038134156651572224747907");

            BigInteger raw = new BigInteger(1, buffer);
            BigInteger encrypted = raw.modPow(exponent, modulus);

            byte[] encrypteddata = encrypted.toByteArray();
            int start = Math.max(0,encrypteddata.length-128);
            int length = Math.min(128, encrypteddata.length);
            System.arraycopy(encrypteddata, start, buffer, 0, length);
        } catch (Exception ex)
        {
            Logger.getLogger(Packet.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /*
     * Alle data ontsleuten met de private key
     */
    public void RSAdecrypt()
    {
        try
        {
            //private exponent
            BigInteger exponent = new BigInteger("76593087762207575883682887208773869121651381098006131253776692375677302277836179455180187366901136667951817215464756593171653012266350501557722792732478062988392269942259024689285491648917186497396093013932723792609119268512250541915741005297805638487747749730364000190772904587668393429145565491707546972673");
            BigInteger modulus = new BigInteger("99379948379960362318133545416777134441212959077806925836047576484354847740695895722711224301417536998763774398137274853428838318449808212642812881950275506403998235108557228615154895317730035593809893475114123023601357966397062390155405161131857926830195384564871250195516504358455352038134156651572224747907");

            BigInteger data = new BigInteger(1, buffer);
            BigInteger raw = data.modPow(exponent, modulus);

            byte[] rawdata = raw.toByteArray();


            int start = Math.max(0,rawdata.length-128);
            int length = Math.min(128, rawdata.length);
            System.arraycopy(rawdata, start, buffer, 0, length);
        } catch (Exception ex)
        {
            Logger.getLogger(Packet.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public enum Error
    {

        WRONG_ACCOUNT(0x01), DAY_LIMIT_REACHED(0x02), WRONG_PIN(0x03), CARD_BLOCKED(0x04),
        BANK_OFFLINE(0x05), ACCOUNT_LIMIT_REACHED(0x06), CONNECTION_LOST(0x07),
        TRANSACTION_TIMEOUT(0x20),
        UNKNOWN_HEADER(0x40), UNKNOWN_ERROR(0x41), UNKNOWN_TRANSACTION(0x42),
        WRONG_BODY(0x50), WRONG_HEADER(0x51), WRONG_ERROR(0x52),
        BANK_NUMBER_TAKEN(0x60), BANK_NUMBER_NOT_ALLOWED(0x61), SERVER_FULL(0x62),
        EQUENS_ERROR(0xD0);
        private final int value;

        Error(int value)
        {
            this.value = value;
        }

        public static Error get(int error)
        {
            for (Error a : Error.values())
            {
                if (a.value() == error)
                {
                    return a;
                }
            }
            return null;
        }

        public int value()
        {
            return value;
        }
    }

    public enum Header
    {

        LOGIN(0x01), LOGOUT(0x02), START_TRANSACTION(0x03), END_TRANSACTION(0x04),
        CONTROL_PIN(0x05), PIN_AMOUNT(0x06), REQUEST_AMOUNT(0x07), ACCEPT_TRANSACTION(0x08),
        EQUENS_RESTARTING(0x20), EQUENS_SHUTDOWN(0x21), ERROR(0xD0);
        private final byte value;

        Header(int value)
        {
            this.value = (byte) value;
        }

        public static Header get(byte header)
        {
            for (Header a : Header.values())
            {
                if (a.value() == header)
                {
                    return a;
                }
            }
            return null;
        }

        public byte value()
        {
            return value;
        }
    }
}